﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Reflection;
using SessionHunter.Model;

namespace SessionHunter.Business
{
    /// <summary>
    /// Manages all of the dynamic references we add, which allow us to deserialize the data in the session
    /// </summary>
    public class ReferenceManager
    {
        private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();

        private List<ExternalReference> _references = null;

        /// <summary>
        /// List of current references and their status
        /// </summary>
        public List<ExternalReference> CurrentReferences
        {
            get
            {
                return _references;
            }
        }

        /// <summary>
        /// Loads an assembly into the current domain, if it is not already
        /// </summary>
        /// <param name="pathToAssembly">The full path to assembly.</param>
        public virtual void AddDynamicReference(string pathToAssembly)
        {
            Contract.Assume(!String.IsNullOrWhiteSpace(pathToAssembly));

            if (_references == null)
                _references = new List<ExternalReference>();

            // See if we have already loaded it
            var existingReference = _references.Where(t => t.Path == pathToAssembly).FirstOrDefault();

            var reference = new ExternalReference();
            try
            {
                reference.Path = pathToAssembly;
                reference.FullAssemblyName = LoadAssembly(pathToAssembly);
            }
            catch (System.IO.FileLoadException fle)
            {
                logger.ErrorException("FileLoadException", fle);
                reference.Error = Resources.Strings.ReferenceError_FileLoadException;
            }
            catch (System.IO.FileNotFoundException nfe)
            {
                logger.ErrorException("FileNotFoundException", nfe);
                reference.Error = Resources.Strings.ReferenceError_FileNotFoundException;
            }
            catch (System.BadImageFormatException bif)
            {
                logger.ErrorException("BadImageFormatException", bif);
                reference.Error = Resources.Strings.ReferenceError_BadImageFormatException;
            }
            catch (Exception ex)
            {
                logger.ErrorException(ex.Message, ex);
                reference.Error = String.Format(Resources.Strings.ReferenceError_Exception, ex.Message);
            }

            if (existingReference != null)
            {
                // Existing reference, clear it out of memory and the list
                _references.Remove(existingReference);
                UnloadReference(existingReference.FullAssemblyName);
            }

            // Add our new reference
            _references.Add(reference);
        }

        /// <summary>
        /// Remove the reference from the list and unload it from the current process
        /// </summary>
        /// <param name="fullName">The full name.</param>
        public virtual void UnloadReference(string fullName)
        {
            //TODO: How do we do this?  There is no Assembly.Unload?  Do I need to load it into an AppDomain instead?
        }

        /// <summary>
        /// Adds a list of references into the current domain.
        /// </summary>
        /// <param name="pathsToAssemblies">The paths to assemblies.</param>
        public virtual void AddDynamicReferences(List<string> pathsToAssemblies)
        {
            if (pathsToAssemblies == null)
                return;

            pathsToAssemblies.ForEach(t => AddDynamicReference(t));
        }

        /// <summary>
        /// Removes the dynamic reference.
        /// </summary>
        /// <param name="referenceToRemove">The reference to remove.</param>
        public virtual void RemoveDynamicReference(ExternalReference referenceToRemove)
        {
            if (_references != null && referenceToRemove != null)
            {
                if (_references.Remove(referenceToRemove))
                    logger.Info("Successfully removed reference to [" + referenceToRemove.FullAssemblyName + "]");
                else
                    logger.Error("Failed to remove reference to [" + referenceToRemove.FullAssemblyName + "]");
            }
        }

        /// <summary>
        /// Loads the assembly or raises an exception
        /// </summary>
        /// <param name="pathToAssembly">The path to assembly.</param>
        /// <returns>The full name</returns>
        protected internal virtual string LoadAssembly(string pathToAssembly)
        {
            var assembly = Assembly.LoadFrom(pathToAssembly);
            foreach (var dependency in assembly.GetReferencedAssemblies())
            {
                logger.Debug("Assembly [" + assembly.ManifestModule.Name + "] depends on [" + dependency.Name + "]");
            }
            return assembly.FullName;
        }

        /// <summary>
        /// Removes any items from the missing assemblies list, based on current references that are valid
        /// </summary>
        /// <param name="missingAssemblies">The current missing assemblies.</param>
        public void RemoveMissingAssemblies(ObservableCollection<string> missingAssemblies)
        {
            // If we have nothing missing, or no current assemblies, exit
            if (missingAssemblies == null || missingAssemblies.Count() == 0 || CurrentReferences == null || CurrentReferences.Count() == 0)
                return;

            logger.Info("Trying to remove missing assemblies based on current assemblies.  Current missing assembly count is " + missingAssemblies.Count());
            // Find all items that are not in an error state and match on full assembly name
            var itemsToRemove = missingAssemblies.Intersect(CurrentReferences.Where(t => String.IsNullOrEmpty(t.Error)).Select(t => t.FullAssemblyName)).ToList();
            if (itemsToRemove.Count() > 0)
            {
                logger.Info("Removing " + missingAssemblies.Count() + " from the missing assemblies list");
                foreach (var item in itemsToRemove)
                    missingAssemblies.Remove(item);
            }
        }

        /// <summary>
        /// Gets the referenced assemblies for an external reference we have added.
        /// </summary>
        /// <param name="externalReference">The external reference.</param>
        /// <returns>Empty list or a list of references, alphabetically sorted</returns>
        protected internal virtual List<string> GetReferencedAssemblies(ExternalReference externalReference)
        {
            Contract.Requires(externalReference != null);
            Contract.Ensures(Contract.Result<List<string>>() != null, "Null referenced assembly list returned");

            logger.Info("Getting referenced assemblies for " + externalReference.FullAssemblyName);

            var retVal = new List<string>();

            // Don't try and get dependencies for one in an error state
            if (!String.IsNullOrEmpty(externalReference.Error))
            {
                logger.Info("Not returning referenced assemblies to reference being in error state: " + externalReference.Error);
                return retVal;
            }

            // Find the reference, which should already be loaded.
            Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
            if (asms == null)
            {
                logger.Info("GetReferencedAssemblies could not find any existing references");
                return retVal;
            }

            var referencedAssembly = asms.FirstOrDefault(t => t.FullName == externalReference.FullAssemblyName);
            if (referencedAssembly == null)
            {
                logger.Info("GetReferencedAssemblies could not find reference [" + externalReference.FullAssemblyName + "]");
                return retVal;
            }

            foreach (var dependency in referencedAssembly.GetReferencedAssemblies())
            {
                logger.Debug("Assembly depends on [" + dependency.FullName + "]");
                retVal.Add(dependency.FullName);
            }

            retVal.Sort(); // Sort it so it's easier to add the references

            logger.Info("GetReferencedAssemblies found " + retVal.Count + " dependencies");
            return retVal;
        }
    }
}
